/*
 * Copyright © 2010-2012 Linaro Limited
 *
 * This file is part of the glmark2 OpenGL (ES) 2.0 benchmark.
 *
 * glmark2 is free software: you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 *
 * glmark2 is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * glmark2.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *  Alexandros Frantzis (glmark2)
 *  Jesse Barker
 *  Simon Que
 */
#include "canvas-drm.h"
#include "log.h"
#include "options.h"
#include "util.h"

#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <string.h>
#include <time.h>

#include <fstream>
#include <sstream>
#include <list>

/******************
 * Public methods *
 ******************/

bool
CanvasDRM::reset()
{
    if (!reset_context())
        return false;

    if (!make_current())
        return false;

    if (!supports_gl2()) {
        Log::error("Glmark2 needs OpenGL(ES) version >= 2.0 to run"
                   " (but version string is: '%s')!\n",
                   glGetString(GL_VERSION));
        return false;
    }

    glViewport(0, 0, width_, height_);

    clear();
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);

    return true;
}

bool
CanvasDRM::init()
{
    EGLint major, minor;
    const char *ver, *extensions;
    uint32_t handle, stride;
    int i, retval;
    const char device_name[] = "/dev/dri/card0";

    num_buffers_ = Options::num_buffers;
    current_front_buffer_index_ = -1;

    signal(SIGINT, &CanvasDRM::quit_handler);

    fd_ = open(device_name, O_RDWR);
    if (fd_ < 0) {
        // Probably permissions error
        fprintf(stderr, "Couldn't open %s, skipping.\n", device_name);
        return false;
    }

    gbm_ = gbm_create_device(fd_);
    if (gbm_ == NULL) {
        fprintf(stderr, "Couldn't create GBM device.\n");
        goto close_fd;
    }

    egl_display_ = eglGetDisplay(gbm_);
    if (egl_display_ == EGL_NO_DISPLAY) {
        fprintf(stderr, "eglGetDisplay() failed.\n");
        goto destroy_gbm_device;
    }

    if (!eglInitialize(egl_display_, &major, &minor)) {
        fprintf(stderr, "eglInitialize() failed.\n");
        goto egl_terminate;
    }

    ver = eglQueryString(egl_display_, EGL_VERSION);
    printf("EGL_VERSION = %s\n", ver);

    extensions = eglQueryString(egl_display_, EGL_EXTENSIONS);
    printf("EGL_EXTENSIONS: %s\n", extensions);

    if (!strstr(extensions, "EGL_KHR_surfaceless_opengl")) {
        printf("No support for EGL_KHR_surfaceless_opengl.\n");
        goto egl_terminate;
    }

    if (!setup_kms(fd_, &kms_))
         goto egl_terminate;

    eglBindAPI(EGL_OPENGL_API);
    egl_context_ = eglCreateContext(egl_display_, NULL, EGL_NO_CONTEXT, NULL);
    if (egl_context_ == NULL) {
        fprintf(stderr, "Failed to create context.\n");
        goto egl_terminate;
    }

    if (!eglMakeCurrent(egl_display_, EGL_NO_SURFACE, EGL_NO_SURFACE,
                        egl_context_)) {
        fprintf(stderr, "Failed to make context current.\n");
        goto destroy_context;
    }

    if (GLExtensions::MapBuffer == NULL && GLExtensions::UnmapBuffer == NULL)
        init_gl_extensions();

    glGenFramebuffers(1, &fb_);
    glBindFramebuffer(GL_FRAMEBUFFER, fb_);

    render_buffers_.resize(num_buffers_);
    buffer_objs_.resize(num_buffers_);
    kms_.fb_ids.resize(num_buffers_);
    images_.resize(num_buffers_);

    for (i = 0; i < num_buffers_; i++) {
        glGenRenderbuffers(1, &render_buffers_[i]);
        glBindRenderbuffer(GL_RENDERBUFFER, render_buffers_[i]);

        buffer_objs_[i] =
            gbm_bo_create(gbm_, kms_.mode.hdisplay, kms_.mode.vdisplay,
                          GBM_BO_FORMAT_XRGB8888,
                          GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);
        if (buffer_objs_[i] == NULL) {
            fprintf(stderr, "Failed to create GBM buffer objects.\n");
            goto unmake_current;
        }
        handle = gbm_bo_get_handle(buffer_objs_[i]).u32;
        stride = gbm_bo_get_pitch(buffer_objs_[i]);

        images_[i] = eglCreateImageKHR(egl_display_, NULL,
                                       EGL_NATIVE_PIXMAP_KHR,
                                       buffer_objs_[i], NULL);
        if (images_[i] == EGL_NO_IMAGE_KHR) {
            fprintf(stderr, "Failed to create EGL image.\n");
            goto destroy_gbm_bo;
        }

#ifdef GL_OES_EGL_image
        glEGLImageTargetRenderbufferStorageOES(GL_RENDERBUFFER, images_[i]);
#else
        fprintf(stderr, "GL_OES_EGL_image was not found at compile time.\n");
#endif

        glGenRenderbuffers(1, &depth_rb_);
        glBindRenderbuffer(GL_RENDERBUFFER, depth_rb_);
        glRenderbufferStorage(GL_RENDERBUFFER,
                              GL_DEPTH_COMPONENT,
                              kms_.mode.hdisplay, kms_.mode.vdisplay);
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                                  GL_RENDERBUFFER,
                                  depth_rb_);

        retval = drmModeAddFB(fd_, kms_.mode.hdisplay, kms_.mode.vdisplay,
                              24, 32, stride, handle, &kms_.fb_ids[i]);
        if (retval) {
            fprintf(stderr, "Failed to create framebuffer.\n");
            goto rm_rb;
        }
        available_buffers_.push(i);
    }

    current_back_buffer_index_ = available_buffers_.front();
    available_buffers_.pop();
    saved_crtc_ = drmModeGetCrtc(fd_, kms_.encoder->crtc_id);
    if (saved_crtc_ == NULL)
        goto rm_fb;

    if (!reset())
    {
        fprintf(stderr, "failed to reset canvas!\n");
        goto drm_mode_free_crtc;
    }

    glFramebufferRenderbuffer(GL_FRAMEBUFFER,
                              GL_COLOR_ATTACHMENT0,
                              GL_RENDERBUFFER,
                              render_buffers_[current_back_buffer_index_]);
    if ((retval = glCheckFramebufferStatus(GL_FRAMEBUFFER)) !=
        GL_FRAMEBUFFER_COMPLETE) {
        fprintf(stderr, "framebuffer not complete: %x\n", retval);
        goto drm_mode_free_crtc;
    }
    resize_no_viewport(width_, height_);
    return true;

drm_mode_free_crtc:
    drmModeFreeCrtc(saved_crtc_);
rm_rb:
    glFramebufferRenderbuffer(GL_FRAMEBUFFER,
                              GL_COLOR_ATTACHMENT0,
                              GL_RENDERBUFFER, 0);
    glBindRenderbuffer(GL_RENDERBUFFER, 0);
    for (i = 0; i < num_buffers_; i++)
      glDeleteRenderbuffers(1, &render_buffers_[i]);

    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                              GL_RENDERBUFFER, 0);
    glDeleteRenderbuffers(1, &depth_rb_);
rm_fb:
    for (i = 0; i < num_buffers_; i++) {
        drmModeRmFB(fd_, kms_.fb_ids[i]);
        eglDestroyImageKHR(egl_display_, images_[i]);
    }
destroy_gbm_bo:
    for (i = 0; i < num_buffers_; i++)
        gbm_bo_destroy(buffer_objs_[i]);
unmake_current:
    eglMakeCurrent(egl_display_, EGL_NO_SURFACE, EGL_NO_SURFACE,
                   EGL_NO_CONTEXT);
destroy_context:
    eglDestroyContext(egl_display_, egl_context_);
egl_terminate:
    eglTerminate(egl_display_);
destroy_gbm_device:
    gbm_device_destroy(gbm_);
close_fd:
    close(fd_);

    return false;
}


CanvasDRM::~CanvasDRM() {
    int i, ret;
    ret = drmModeSetCrtc(fd_, saved_crtc_->crtc_id, saved_crtc_->buffer_id,
                         saved_crtc_->x, saved_crtc_->y,
                         &kms_.connector->connector_id, 1, &saved_crtc_->mode);
    if (ret) {
        fprintf(stderr, "Failed to restore crtc: %d\n", ret);
    }

    drmModeFreeCrtc(saved_crtc_);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER,
                              GL_COLOR_ATTACHMENT0,
                              GL_RENDERBUFFER, 0);
    glBindRenderbuffer(GL_RENDERBUFFER, 0);
    for (i = 0; i < num_buffers_; i++)
        glDeleteRenderbuffers(1, &render_buffers_[i]);

    glFramebufferRenderbuffer(GL_FRAMEBUFFER,
                              GL_DEPTH_ATTACHMENT,
                              GL_RENDERBUFFER, 0);
    glDeleteRenderbuffers(1, &depth_rb_);

    for (i = 0; i < num_buffers_; i++) {
        drmModeRmFB(fd_, kms_.fb_ids[i]);
        eglDestroyImageKHR(egl_display_, images_[i]);
    }
    for (i = 0; i < num_buffers_; i++)
        gbm_bo_destroy(buffer_objs_[i]);
    eglMakeCurrent(egl_display_, EGL_NO_SURFACE, EGL_NO_SURFACE,
                   EGL_NO_CONTEXT);
    eglDestroyContext(egl_display_, egl_context_);
    eglTerminate(egl_display_);
    gbm_device_destroy(gbm_);
    close(fd_);
}

void
CanvasDRM::visible(bool /* visible */)
{

}

void CanvasDRM::clear()
{
    glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
#if USE_GL
    glClearDepth(1.0f);
#elif USE_GLESv2
    glClearDepthf(1.0f);
#endif
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}

void CanvasDRM::update()
{
    if (Options::swap_buffers)
        swap_buffers();
    else
        glFinish();
}

void CanvasDRM::print_info()
{
    make_current();

    std::stringstream ss;
    ss << "    OpenGL Information" << std::endl;
    ss << "    GL_VENDOR:     " << glGetString(GL_VENDOR) << std::endl;
    ss << "    GL_RENDERER:   " << glGetString(GL_RENDERER) << std::endl;
    ss << "    GL_VERSION:    " << glGetString(GL_VERSION) << std::endl;
    Log::info("%s", ss.str().c_str());
}

Canvas::Pixel CanvasDRM::read_pixel(int x, int y)
{
    uint8_t pixel[4];
    glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
    return Canvas::Pixel(pixel[0], pixel[1], pixel[2], pixel[3]);
}

void CanvasDRM::write_to_file(std::string &filename)
{
    char *pixels = new char[width_ * height_ * 4];

    for (int i = 0; i < height_; i++) {
        glReadPixels(0, i, width_, 1, GL_RGBA, GL_UNSIGNED_BYTE,
                     &pixels[(height_ - i - 1) * width_ * 4]);
    }

    std::ofstream output (filename.c_str(), std::ios::out | std::ios::binary);
    output.write(pixels, 4 * width_ * height_);

    delete [] pixels;
}

bool
CanvasDRM::should_quit()
{
    return should_quit_;
}

void
CanvasDRM::resize(int width, int height)
{
    resize_no_viewport(width, height);
    glViewport(0, 0, width_, height_);
}

int
CanvasDRM::get_frame_buffer() {
    return fb_;
}

/*********************
 * Protected methods *
 *********************/

bool CanvasDRM::supports_gl2()
{
    std::string gl_version_str(
        reinterpret_cast<const char*>(glGetString(GL_VERSION)));
    int gl_major = 0;

    size_t point_pos(gl_version_str.find('.'));

    if (point_pos != std::string::npos) {
        point_pos--;

        size_t start_pos(gl_version_str.rfind(' ', point_pos));
        if (start_pos == std::string::npos)
            start_pos = 0;
        else
            start_pos++;

        gl_major = Util::fromString<int>(
                gl_version_str.substr(start_pos, point_pos - start_pos + 1)
                );
    }

    return gl_major >= 2;
}

bool CanvasDRM::make_current()
{
    if (!ensure_egl_surface())
        return false;

    if (!ensure_egl_context())
        return false;

    if (egl_context_ == eglGetCurrentContext())
        return true;

    if (!eglMakeCurrent(egl_display_, egl_surface_, egl_surface_,
        egl_context_)) {
        Log::error("Error: eglMakeCurrent failed with error %d\n",
                   eglGetError());
        return false;
    }

    if (!eglSwapInterval(egl_display_, 0))
        Log::info("** Failed to set swap interval. Results may be bounded above"
                  " by refresh rate.\n");
    init_gl_extensions();
    return true;
}
bool CanvasDRM::reset_context()
{
    if (!ensure_egl_display())
        return false;

    if (!egl_context_)
        return true;

    if (eglDestroyContext(egl_display_, egl_context_) == EGL_FALSE) {
        Log::debug("eglDestroyContext() failed with error: 0x%x\n",
                   eglGetError());
    }

    egl_context_ = 0;
    return true;
}
extern unsigned int *nitrous_timestamps;

void CanvasDRM::swap_buffers()
{
    int ret;

    rendered_buffers_.push(current_back_buffer_index_);

    pollfd poll_data;
    poll_data.fd = fd_;
    poll_data.events = POLLIN;

    // Check the display controller to see if it has data to be read back.
    ret = poll(&poll_data, 1, 0);
    // If there is display controller data available, or there are no more
    // free buffers, handle the display controller data.
    if (ret > 0 || available_buffers_.empty()) {
        // Make sure there is data available.
        if (ret <= 0)
            while (poll(&poll_data, 1, -1) <= 0);

        // Handle the display controller page flip event(s).
        drmEventContext event_context;
        memset(&event_context, 0, sizeof event_context);
        event_context.version = DRM_EVENT_CONTEXT_VERSION;
        event_context.page_flip_handler = CanvasDRM::page_flip_handler;
        drmHandleEvent(fd_, &event_context);
    }
    // Get the next buffer for rendering.
    current_back_buffer_index_ = available_buffers_.front();
    available_buffers_.pop();
    glFramebufferRenderbuffer(GL_FRAMEBUFFER,
                              GL_COLOR_ATTACHMENT0,
                              GL_RENDERBUFFER,
                              render_buffers_[current_back_buffer_index_]);
    if ((ret = glCheckFramebufferStatus(GL_FRAMEBUFFER)) !=
        GL_FRAMEBUFFER_COMPLETE) {
        fprintf(stderr, "framebuffer not complete: %x\n", ret);
        return;
    }

    // Attempt to flip to the first rendered buffer in the queue.
    if (current_front_buffer_index_ < 0) {
        glFlush();
        ret = drmModePageFlip(fd_, kms_.encoder->crtc_id,
                              kms_.fb_ids[rendered_buffers_.front()],
                              DRM_MODE_PAGE_FLIP_EVENT, this);
        if (!ret) {
            // If flip was successful, take it out of the queue.
            current_front_buffer_index_ = rendered_buffers_.front();
            rendered_buffers_.pop();
        }
    }
}

bool
CanvasDRM::ensure_egl_display()
{
    if (egl_display_)
        return true;

    egl_display_ = eglGetDisplay((EGLNativeDisplayType) egl_display_);
    if (!egl_display_) {
        Log::error("eglGetDisplay() failed with error: %d\n",
                   eglGetError());
        return false;
    }

    if (!eglInitialize(egl_display_, NULL, NULL)) {
        Log::error("eglInitialize() failed with error: %d\n",
                   eglGetError());
        return false;
        egl_display_ = 0;
    }

    return true;
}

bool
CanvasDRM::ensure_egl_config()
{
    static const EGLint attribs[] = {
        EGL_RED_SIZE, 1,
        EGL_GREEN_SIZE, 1,
        EGL_BLUE_SIZE, 1,
        EGL_ALPHA_SIZE, 1,
        EGL_DEPTH_SIZE, 1,
#if USE_GLESv2
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
#elif USE_GL
        EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
#endif
        EGL_NONE
    };
    EGLint num_configs;
    EGLint vid;

    if (egl_config_)
        return true;

    if (!ensure_egl_display())
        return false;

    if (!eglChooseConfig(egl_display_, attribs, &egl_config_, 1,
                         &num_configs)) {
        Log::error("eglChooseConfig() failed with error: %d\n", eglGetError());
        return false;
    }

    if (!eglGetConfigAttrib(egl_display_, egl_config_,
                            EGL_NATIVE_VISUAL_ID, &vid)) {
        Log::error("eglGetConfigAttrib() failed with error: %d\n",
                   eglGetError());
        return false;
    }

    if (Options::show_debug) {
        int buf, red, green, blue, alpha, depth, id, native_id;
        eglGetConfigAttrib(egl_display_, egl_config_, EGL_CONFIG_ID, &id);
        eglGetConfigAttrib(egl_display_, egl_config_, EGL_NATIVE_VISUAL_ID,
                           &native_id);
        eglGetConfigAttrib(egl_display_, egl_config_, EGL_BUFFER_SIZE, &buf);
        eglGetConfigAttrib(egl_display_, egl_config_, EGL_RED_SIZE, &red);
        eglGetConfigAttrib(egl_display_, egl_config_, EGL_GREEN_SIZE, &green);
        eglGetConfigAttrib(egl_display_, egl_config_, EGL_BLUE_SIZE, &blue);
        eglGetConfigAttrib(egl_display_, egl_config_, EGL_ALPHA_SIZE, &alpha);
        eglGetConfigAttrib(egl_display_, egl_config_, EGL_DEPTH_SIZE, &depth);
        Log::debug("EGL chosen config ID: 0x%x Native Visual ID: 0x%x\n"
                   "  Buffer: %d bits\n"
                   "     Red: %d bits\n"
                   "   Green: %d bits\n"
                   "    Blue: %d bits\n"
                   "   Alpha: %d bits\n"
                   "   Depth: %d bits\n",
                   id, native_id,
                   buf, red, green, blue, alpha, depth);
    }

    return true;
}

bool
CanvasDRM::ensure_egl_context()
{
    return true;
}

bool
CanvasDRM::ensure_egl_surface()
{
    return true;
}

void
CanvasDRM::init_gl_extensions()
{
#if USE_GLESv2
    /*
     * Parse the extensions we care about from the extension string.
     * Don't even bother to get function pointers until we know the
     * extension is present.
     */
    std::string extString;
    const char* exts =
        reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS));
    if (exts)
        extString = exts;

    if (extString.find("GL_OES_mapbuffer") != std::string::npos) {
        GLExtensions::MapBuffer = reinterpret_cast<PFNGLMAPBUFFEROESPROC>(
            eglGetProcAddress("glMapBufferOES"));
        GLExtensions::UnmapBuffer = reinterpret_cast<PFNGLUNMAPBUFFEROESPROC>(
            eglGetProcAddress("glUnmapBufferOES"));
    }
#elif USE_GL
    GLExtensions::MapBuffer = glMapBuffer;
    GLExtensions::UnmapBuffer = glUnmapBuffer;
#endif
}


/*******************
 * Private methods *
 *******************/

void
CanvasDRM::resize_no_viewport(int width, int height)
{
    width_ = width;
    height_ = height;

    projection_ =
        LibMatrix::Mat4::perspective(60.0, width_ / static_cast<float>(height_),
                                     1.0, 1024.0);
}

EGLBoolean CanvasDRM::setup_kms(int fd_, struct CanvasDRM::kms *kms_)
{
    drmModeRes *resources = NULL;
    drmModeConnector *connector = NULL;
    drmModeEncoder *encoder = NULL;
    int i;

    resources = drmModeGetResources(fd_);
    if (!resources) {
        fprintf(stderr, "drmModeGetResources failed\n");
        return EGL_FALSE;
    }

    for (i = 0; i < resources->count_connectors; i++) {
        connector = drmModeGetConnector(fd_, resources->connectors[i]);
        if (connector == NULL)
            continue;

        if (connector->connection == DRM_MODE_CONNECTED &&
        connector->count_modes > 0)
            break;

        drmModeFreeConnector(connector);
    }

    if (i == resources->count_connectors) {
        fprintf(stderr, "No currently active connector found.\n");
        return EGL_FALSE;
    }

    for (i = 0; i < resources->count_encoders; i++) {
        encoder = drmModeGetEncoder(fd_, resources->encoders[i]);

    if (encoder == NULL)
        continue;

    if (encoder->encoder_id == connector->encoder_id)
        break;

        drmModeFreeEncoder(encoder);
    }

    kms_->connector = connector;
    kms_->encoder = encoder;
    kms_->mode = connector->modes[0];

    return EGL_TRUE;
}


void CanvasDRM::page_flip_handler(int /* fd */, unsigned int /* frame */,
    unsigned int /* sec */, unsigned int /* usec */, void* data)
{
    CanvasDRM* canvas = reinterpret_cast<CanvasDRM*> (data);
    canvas->available_buffers_.push(canvas->current_front_buffer_index_);
    canvas->current_front_buffer_index_ = -1;
}

bool CanvasDRM::should_quit_ = false;

void CanvasDRM::quit_handler(int /* signum */)
{
    should_quit_ = true;
}
